# -*- coding: utf-8 -*-

import os
import copy
from  mathematic import Vector
from  mathematic import Matrix
import mathematic

const = 0    
class Curve(object):
    """
        This class refers to the generic parametric curves.
    """

    def __init__(self, cp_list, bbox_type, degree):
        """
            The attributes of a Curve object are initialized by parameters
            passed in this method. These are specificated as follow:
            
            
            * cp_list: refers a list of control points (base points to
                       parameterization)
            * degree : This attribute determine the quantity of control points
                       are necessary by each patch (degree + 1)
            * patch  : a list of control points that will considered in one  
                       patch of curve generation. There are degree + 1 points by 
                       patch. This is update to generate each patch of a curve.
            * bbox   : this attribute is a composition of a class that inherits
                       of BoundingBox class. There are differents types of
                       implementations for this attribute. The one used is 
                       defined on instanciation of the object of type of curve 
                       that will be used.
            * bbox_list: list of bounding box objects.           
        """
        self.cp_list = cp_list
        self.degree  = degree
        self.patch   = self.cp_list[:(self.degree+1)]        
        self.bbox_type = bbox_type
        self.bbox_list = []

    def showControlPoints(self):
        """
            This method show a all points of the attribute control_point_list
        """
        for point in self.cp_list:
            print "(%s, %s, %s)" % (point[0], point[1], point[2])

    
    def showPatch(self):
        """
            This method show a all points on current patch (current status 
            of the attribute patch)
        """
        for point in self.patch:
            print "(%s, %s, %s)" % (point[0], point[1], point[2])

        
    def nextPatch(self):
        """
            Alter the state of atributte patch going to the next patch.
            
            To do it, remove the first element and insert the next in control
            point list (attribute control_points). 
            
            Coming to the end of the list, it is considered as the next
            element the first element of this.
        """
        index = self.cp_list.index(self.patch[-1])
        self.patch.remove(self.patch[0])
        self.patch.append( self.cp_list[ (index+1) % len(self.cp_list) ] )
        return self.patch


    def definePatch(self, index = 0):
        """
            This method define the patch that will be used to draw the curve.
        """
        self.patch = [self.cp_list[index + i] for i in range(4)]
        return self.patch

    
    def generatePatch(self):
        """
            TODO
        """
        pass



class BoundingBox(object):
    """
        TODO
    """
    
    def __init__(self):
        """
            A bounding box is a box that involves a set of points. 
            
            This box is defined by: origin point (the left-top-front vertex) and
            by vectors width, height and depth. These vectors determines the 
            system coordinates of the box. Moreover, its modules determines the
            dimensions of the box.
            
            Other important attribute is cpp_list (list of control points of the 
            patch represented by bbox object)
        """
        self.origin = Vector( [0.0, 0.0, 0.0] )
        self.width  = Vector( [0.0, 0.0, 0.0] )
        self.height  = Vector( [0.0, 0.0, 0.0] )
        self.depth  = Vector( [0.0, 0.0, 0.0] )

        # list of control points of the patch
        self.cpp_list = []   

    def isPointInBBox(self, point):
        """
            This method defines if the pointis inside or not of the bbox.
        """
        pass

    def calculateAxis(self, point_list):
        """
            This method calculate the axis and dimension of a bbox.
        """
        pass

    def calculateBoundingBox(self, point_list):
        """
            TODO
        """
        return self.calculateAxis(point_list)



class GlobalBoundingBox(BoundingBox):

    """
        This class refers the bounding box oriented by global coordinates
        system.
    """

    def calculateAxis(self, point_list):
        """
            This method calculate the axis and dimension of a bbox.
        """
        x_axis = [point[0] for point in point_list]
        y_axis = [point[1] for point in point_list]
        z_axis = [point[2] for point in point_list]
        
        self.origin = Vector( [min(x_axis), min(y_axis), min(z_axis) ] )
        self.width  = Vector( [max(x_axis) - self.origin[0], 0.0, 0.0] )
        self.height = Vector( [0.0, max(y_axis) - self.origin[1], 0.0] )
        self.depth  = Vector( [0.0, 0.0, max(z_axis) - self.origin[2]] )
        return self

    def isPointInBBox(self, point):
        """
            This method defines if the pointis inside or not of the bbox.
        """
        left  =  self.origin[0] 
        right =  (self.origin + self.width)[0]
        up    =  self.origin[1]
        down  =  (self.origin + self.height)[1]
        near  =  self.origin[2] 
        far   =  (self.origin + self.depth)[2]
        if ( left -  0.1) <= point[0] <= ( right +  0.1) and \
           ( up   -  0.1) <= point[1] <= ( down  +  0.1) and \
           ( near -  0.1) <= point[2] <= ( far   +  0.1):
            return True
        else:
            return False

class OrientedBoundingBox(BoundingBox):
    """
        TODO
    """
    
    def isPointInBBox(self, point):
        """
            This method defines if the pointis inside or not of the bbox.
        """
        #TODO - to implement this after the method calculateAxis is ready
        pass

    def calculateCentroide(self, point_list):
        """
            This method return the points it is the avarage of the sampling of
            points (the centroide of set of points). This point is centroide of 
            the box.
        """
        return mathematic.Statistical().centroide(point_list)

    def calculateAxis(self):
        """
            This method calculates the axis of oriented bounding box. This axis 
            is the eigenvectors of matrix os covariance of the sampling of 
            points. Statistically verify that eigenvectors of this matrix (and
            by spectral theorem, as the matrix is symmetric, its eigenvectors 
            formed form a basis) is a base, so can be considered a coordinate 
            system, that the sum of modules of vectors (vectors that represents 
            the points of sampling in new coordinates system)is minimal.
        """
        math = mathematic.Statistical()
        covariance = math.covariance(point_list)
        #TODO - 1: Calculate the eigenvectors
        #TODO - 2: Change the coordinates of the points to the new coordinate 
        #          system (formed by eigenvectors find in step previous)
        #TODO - 3: Calculate the extremes points
        #TODO - 4: Forms the bbox with the extreme points find in the previous
        #          step     

class FactoryBBox(object):

    def createBBox(self, type_bbox, cpp_list):        
        if type_bbox == "Global":
            bbox = GlobalBoundingBox()
        elif type_bbox == "Oriented":
            bbox = OrientedBoundingBox()
        bbox.cpp_list = cpp_list
        return bbox

class CubicBSplineCurve(Curve):
    """
        B-Spline is one type of parameterization of curve and surfaces.
        This implementation refers to B-Splines cubics and periodics.
        
        A B-Spline Curve is generated by multiplication of:
        [t³ t² t 1] * [base_matrix] * transpose(control_point_list)
        The list of parameters is generated by a method parametricList(t)
        
        A B Spline Surface is generated by multiplication of:
        point = [t³ t² t 1] * [base_matrix] * [control_point_matrix] * 
                transpose([base_matrix]) * transpose([r³ r² r 1])
        The first of parameters is generated by a method parametricList(t),
        and the second is obtained by parametricList(r).
        
        This parameters is obtained based on density of points in patch.
    """
    
    def __init__(self, cp_list, bbox_type):
        """
            On initialize a bspline curve object define the matrix that 
            determines the basis of interpolation method how is doing as follow.
        
            The matrix of B-Spline curves is (1/6) of the matrix showed
        """
        super(CubicBSplineCurve, self).__init__(cp_list, bbox_type, 3)
        self.base_matrix = Matrix ([[-1.0,  3.0, -3.0,  1.0],
                            	    [ 3.0, -6.0,  3.0,  0.0],
                                    [-3.0,  0.0,  3.0,  0.0],
	                                [ 1.0,  4.0,  1.0,  0.0]])
        self.base_matrix *= 1.0/6.0

    def parametricList(self, t):
        """
            This method return a list of parameter according to type curve.
            
            If  this object referes a bspline curve this list is: [t³ t² t 1]
            If refers to derivative of bspline curve, this list is: [3t² 2t 1 0]
            And, if refres to second derivative of bspline curve, this list is:
            [6t 2 0 0]
        """
        pass

    def calculateBase(self, t):
        return self.parametricList(t) * self.base_matrix
       
    def calculatePoint(self, t):
        """
            This method calculates the coordinates of a point of the patch 
            generated by control points of the attribute self.patch on 
            parameter t.
        """
        return self.calculateBase(t) * self.patch

        
    def generatePatch(self, density = 10):
        """
            A patch of B-Spline Curve is generated by multiplication of:
            [t³ t² t 1] * [base_matrix] * transpose(control_point_list)
            
            This method generates a quantity of points determined by parameter 
            density. To each parameter (k / (density - 1) with k between 0 and 
            density) calculates the point of curve (by method curveBSplineCubic)
            in this parameter. So the list with all points is generated
        """
        point_patch_list = [ self.calculatePoint( float(k) / float(density - 1))
                                             for k in range(density) ] 
        
        bbox = FactoryBBox().createBBox(self.bbox_type, copy.copy(self.patch))
        bbox = bbox.calculateBoundingBox(point_patch_list)
        self.bbox_list.append(bbox)
        return point_patch_list

    def manipulateCurve(self, point_curve, new_point):
        """
            This method delegatesto de object of CurveManipulate the action of 
            manipulation of the geometry of curve.
            
            The parameter passed define the point of curve that will be modified
            (before is necessary verify if this point is inside curve) and the 
            new position of this point. The manipulate action consist in 
            reposition the control points of the patch of this point. So is 
            returned a list of this new position of this control points.
        """
        manipulator = CurveManipulate(self)
        return manipulator.manipulate(point_curve, new_point)



class BSplineFactory(object):
    """
        This class is the design pattern Factory. This choice how type of 
        bspline objects will be instancied. The unique diference of a curve 
        bspline, derivative or second derivative of this curve is the method 
        parametricList, as explained on class CubicBSplineCurve.
    """

    def createBSplineCurve(self, type_curve, cp_list, bbox):
        """
            Choice the type of bpline curve.
        """
        bspline = CubicBSplineCurve(cp_list, bbox)
        def parametricList(t):
            if type_curve == "CURVE":
                    return Vector( [t*t*t, t*t, t, 1] )
            elif type_curve == "DERIVATIVE":
                    return Vector( [3*t*t, 2*t, 1, 0] )
            elif type_curve == "SECOND_DERIVATIVE":
                    return Vector( [6*t, 2, 0, 0]     )
        bspline.parametricList = parametricList
        return bspline


            
class CurveManipulate(object):
    """
        TODO
    """
    const = 0
    def __init__(self, curve):
        """
            This attribute curve is a object of one type of implementation of
            class curve.
        """
        self.curve = curve
        
    def isPointInBBoxList(self, point):
        """
            This method verifies if the point passed by parameter is inside of
            region defined by a bounding box of the curve this represents.
            
            One point can be in intersection of more one bbox. So this method 
            return a list with all bbox that containing the point.
        """    
        return [ bbox for bbox in self.curve.bbox_list 
                                                  if bbox.isPointInBBox(point) ]

    def isPoinrInCurve(self, point):
        pass
    
    def findPointInCurve(self, point):
        """
            This method verifies if the point is on the curve, and if is, then
            find the patch, and more, the parameter that generate this point.
            
            As a point can be in more one bbox, this procedure is doing to all 
            bbox that containing this point.
            
            To find the parameter is used the Newton Method to find zeros of a
            function of one variable. In this method the value is determined by:
                             t(i+1) = ti - f(ti) / f'(ti)

            This iterative method necessaries of a first inicial value (t=0.5).
            
            The function used in this method is the inner product of the vector
            determined by difference of the point and the point on curve on 
            parameter t (in each iterate) and the derivative of the curve, ie,
                            f(ti) = <P - c(ti), c'(ti)>.
            And the f'(ti) = <P - c(ti), c''(ti)> + (-1)<c'(ti), c'(ti)>
        """
        t = 0.5
        bbox_list = self.isPointInBBoxList(point)
        if bbox_list:
            for bbox in bbox_list:
                self.curve.patch = bbox.cpp_list
                def function(t):
                    curve_bspline = BSplineFactory().createBSplineCurve( 
                                               "CURVE", self.curve.patch, None )
                    curve = curve_bspline.calculatePoint
                    
                    derivative_bspline = BSplineFactory().createBSplineCurve(
                                          "DERIVATIVE", self.curve.patch, None )
                    tangent = derivative_bspline.calculatePoint

                    return (point - curve(t)).innerProduct( tangent(t) )

                
                def derivative(t):
                    curve_bspline = BSplineFactory().createBSplineCurve( 
                                               "CURVE", self.curve.patch, None )
                    curve = curve_bspline.calculatePoint
                    
                    derivative_bspline = BSplineFactory().createBSplineCurve(
                                          "DERIVATIVE", self.curve.patch, None )
                    tangent = derivative_bspline.calculatePoint

                    second_derivative_bspline =                                \
                        BSplineFactory().createBSplineCurve("SECOND_DERIVATIVE",
                                                        self.curve.patch, None )
                    normal = second_derivative_bspline.calculatePoint


                    return point.innerProduct( normal(t) ) -              \
                           curve(t).innerProduct( normal(t) )      -      \
                           tangent(t).norma() ** 2
                
                t = mathematic.newton(t, function, derivative )
                
                projection = self.curve.calculatePoint(t)
                if 0.0 <= t <= 1 and (projection - point).norma() < 0.05:
                    return projection, t, bbox.cpp_list
            else:
                return None
               
    def manipulate(self, point, new_position):
        """
            delta(CPP) = ( displacement / (base * baseT) ) * baseT
        """
        values = self.findPointInCurve(point)
        if values:
            curve_point, parameter, cpp_list = values
            if 0.0 <= parameter <= 1 and (curve_point - point).norma() < 0.05:
                displacement = Vector(new_position) - point
                base = self.curve.calculateBase(parameter)
                b_bT = base.innerProduct(base)
                
                new_cpp_list = []
                for i in range(4):
                    new_cpp_list.append( 
                           [ cpp_list[i][j] + (displacement[j] * base[i] / b_bT) 
                                                           for j in range(3) ] )
                if new_cpp_list:
                    for i in range(len(self.curve.cp_list)):
                        if self.curve.cp_list[i] in cpp_list:
                            self.curve.cp_list[i] = new_cpp_list[ 
                                         cpp_list.index(self.curve.cp_list[i]) ]
        else:
            return None
